[UE4]Networking in Basic 您所在的位置:网站首页 ue4 replication [UE4]Networking in Basic

[UE4]Networking in Basic

#[UE4]Networking in Basic| 来源: 网络整理| 查看: 265

keywords:UE4, Replication, Relicate, Reliable, RPC, RTS Movement, Dedicated Server, 专用服务器

实例的完整工程下载地址见文章底部。

属性同步 属性同步基本用法

步骤: 1,对属性添加UPROPERTY(Replicated)宏:

//Player display name UPROPERTY(Replicated) FString Alias_;

2,属性所在的class中重写函数GetLifetimeReplicatedProps: 需要头文件:

#include "Net/UnrealNetwork.h"

重写函数:

void AReplTestCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AReplTestCharacter, Alias_); } 属性同步扩展用法

如何为同步属性添加回调函数?

用法:

UPROPERTY(ReplicatedUsing = OnHPChanged) float HealthPoint; UFUNCTION() void OnHPChanged();

ReplicatedUsing只在客户端执行,服务端不执行。客户端相关表现逻辑可以放在ReplicatedUsing回调函数。另外函数需要添加UFUNCTION。

蓝图对应的标记叫RepNotify https://docs.unrealengine.com/en-US/Resources/ContentExamples/Networking/1_4/index.html

RPC(远程执行调用)

步骤: 1,对需要远程执行的函数添加宏UFUNCTION(Server, Reliable, WithValidation)或者UFUNCTION(Client, Reliable)。其中Server表示在客户端调用,在服务端执行;WithValidation表示是否需要验证函数,加上的画需要添加函数:bool MyFun_Validate(),函数体内容写在MyFun_Implementation函数内。cpp中不需要与函数名同名的函数体,只需要实现_Validate和_Implementation两个函数即可。 头文件:

//移动角色(只在服务端执行的函数) UFUNCTION(Server, Reliable, WithValidation) void ServerMoveToDest(APawn* Panw, const FVector DestLocation); bool ServerMoveToDest_Validate(APawn* Panw, const FVector DestLocation); void ServerMoveToDest_Implementation(APawn* Panw, const FVector DestLocation);

CPP:

bool AReplTestPlayerController::ServerMoveToDest_Validate(APawn* Panw, const FVector DestLocation) { return true; } void AReplTestPlayerController::ServerMoveToDest_Implementation(APawn* Pawn, const FVector DestLocation) { //logic... } 三种 RPC 函数区别 UFUNCTION(Server, Reliable, WithValidation) 客户端请求,服务端执行。 使用场景:涉及到数据安全的行为,比如:砍一刀扣血,扣血条件判定以及血量修改,都应该放在服务端执行。 UFUNCTION(Client, Reliable) 服务端请求,客户端执行,也可能是服务端执行(详细描述见下文)。NM_Standalone模式下,Client函数也会执行。 使用场景:只是表现相关,不涉及数据修改的行为,比如:装备升级,整备的属性修改发生在服务端,升级成功后的外观变化,服务端需要通知客户端替换武器 Mesh 和材质。 UFUNCTION(NetMulticast, Reliable) 服务端先执行,然后所有连接的客户端再执行。默认情况下只在服务端执行(详细描述见下文)。 使用场景:当前客户端做的表现也希望其他客户端也看到,比如:播放攻击动作,客户端A控制的角色A播放攻击动作,希望所有其他客户端也能看见角色A播放了攻击动作。 NetMulticast一般可以设置为 Unreliable ,表示如果网络不通畅,不重新发送 UDP 消息,比如上述装备升级,如果网络问题导致客户端未能更新装备外观,影响也不大,Unreliable 可以节省带宽。

UFUNCTION(Client, Unreliable) 并不表示是一定在客户端执行!!!也可能是服务端执行。官方文档: Since the server can own actors itself, a “Run on Owning Client” event may actually run on the server, despite its name.

官方文档:Multiplayer in Blueprints https://docs.unrealengine.com/latest/INT/Gameplay/Networking/Blueprints/index.html 角色身上需要设置的属性

角色蓝图上的这几个属性默认是勾选的。如果是C++,对应的属性名也是这几个。

登陆界面:

多个客户端连上服务端的最终情景:

同步相关的基础概念 NetMode

每个Actor有个接口:AActor::GetNetMode(),返回值意思如下:

NM_Standalone:表示当前Actor在独立服务器(单机模式),可以执行客户端、服务端的所有功能。 NM_DedicatedServer:表示当前Actor在专用服务器上,只能执行服务端相关的功能。 NM_ListenServer:官方文档上解释的很少,我在官方论坛上问了下得到的几个解释是,ListenServer可以被游戏内的某个玩家的机器当作服务器,该服务器拥有操作每个客户端角色的权利。这种模式下,更方便来创建服务器;缺点是承载人数较少。个人理解是ListenServer可能更适合做局域网游戏,因为这种既不需要考虑外挂也不需要考虑承载压力。 NM_Client:当客户端未连接 Dedicated Server 时,NetMode 为 NM_Standalone, 连接成功之后,客户端的 NetMode 就变成了 NM_Client 。 DedicatedServer没有客户端的相关功能,只接收远程客户端的网络请求,适合承载大规模玩家在线。另外连接DedicatedServer的客户端,是没有权限直接修改其他玩家的数据,因为客户端上的大部分Object的Role枚举都是ROLE_AutonomousProxy(如果你逻辑有bug,也是可以被客户端发送非法请求串改其他角色的数据,所以这里所说的没有权限修改,不要以为DedicatedServer可以帮你反外挂),但是在ListenServer的玩家主机上可以,因为他拥有其他所有玩家的实例(Role枚都是ROLE_Authority类型)。UE4的一些API内部有权限检测的逻辑:判断当前Role类型是否为ROLE_Authority,这样避免非法修改数据。

判断当前网络模式是否为Dedicated Server或者Client的API: 蓝图接口:

bool UKismetSystemLibrary::IsServer(const UObject* WorldContextObject) bool UKismetSystemLibrary::IsDedicatedServer(const UObject* WorldContextObject) bool UKismetSystemLibrary::IsStandalone(const UObject* WorldContextObject)

C++接口:

//CoreMisc.h下的全局函数 bool IsRunningDedicatedServer() bool IsRunningClientOnly()

如果通过editor启动游戏:

IsRunningDedicatedServer()返回true的前提是命令行包含参数-server; IsRunningClientOnly()返回true的前提是命令行参数包含-game -clientonly; 打包版本下,BuildTarget必须为Client,IsRunningClientOnly()才会返回true; Role

每个Actor有个公开属性:AActor::Role。表示当前Actor的作用权限,枚举值有:

ROLE_SimulatedProxy:表示当前Actor是一个模拟服务端的Actor状态Object,无法修改服务端上的数据,也没有权限执行远程函数(Reliable标识的UFUNCTION)。DedicatedServer服务器中创建的对象,在服务端都是ROLE_Authority,这些对象映射在客户端上的对象则都是ROLE_SimulatedProxy,当然也有办法让这些对象在客户端变成非ROLE_SimulatedProxy类型,具体方法见下文。该类型的对象,如果去执行自身的UFUNCTION(Server)标识的函数,该函数不会执行。 ROLE_AutonomousProxy:与ROLE_SimulatedProxy相似,区别是ROLE_AutonomousProxy有权限执行远程函数,但是远程函数必须是该Object身上的,其他Object上的远程函数没有权限执行。客户端的ROLE_AutonomousProxy类型对象,其函数若未加UFUNCTION(Server)标识,执行时仍然是在是在客户端。执行UFUNCTION(Server)标识的函数时,在服务端触发执行,Role类型为ROLE_Authority,NetMode类型为NM_DedicatedServer。ROLE_AutonomousProxy的优点之一是:当对角色执行转向或者位移控制时,会自动请求服务端处理,而ROLE_SimulatedProxy是做不到的,需要手动请求服务端。 ROLE_Authority:最高权限,即可以修改Server上的属性,又可以执行任何Objecct身上的远程函数。Standalone服务器中,所有对象都是ROLE_Authority。DedicatedServer服务器中创建的对象,在服务端都是ROLE_Authority。 如果是Client创建的对象,其Role也是ROLE_Authority,但是这种Actor无法与服务端通信。

服务端SpawnActor创建的Character,若开启了Replication,则其在客户端会生成一个ROLE_SimulatedProxy的Character,此时在服务端用其对应的PlayerController对其Possess,则该客户端Character的Role会变成ROLE_AutonomousProxy。

从执行Possess到Role被修改为ROLE_AutonomousProxy的堆栈:

UE4Editor-Engine.dll!AActor::SetAutonomousProxy(const bool bInAutonomousProxy, const bool bAllowForcePropertyCompare) Line 3268 C++ UE4Editor-Engine.dll!APawn::PossessedBy(AController * NewController) Line 511 C++ UE4Editor-Engine.dll!ACharacter::PossessedBy(AController * NewController) Line 822 C++ UE4Editor-Engine.dll!APlayerController::OnPossess(APawn * PawnToPossess) Line 770 C++ UE4Editor-Engine.dll!AController::Possess(APawn * InPawn) Line 293 C++ 同步相关的常见问题 如何让一个ROLE_SimulatedProxy对象变成ROLE_AutonomousProxy

先补充一个概念:服务端连接(非官方文档,自己通过实践后的理解),比如,AGameModeBase::PostLogin(APlayerController* NewPlayer)中的PlayerController就是拥有服务端连接的对象,在服务端,其可以调用Client function且能保证该Client function在客户端执行;在客户端,其也有权限调用 Server function且能保证该Server function在服务端执行。

两种方式:

方式一:3个步骤,缺一不可:

将bReplicates设置为true:Actor->SetReplicates(true);

设置自己的owner(SetOwner())为拥有服务端连接的Actor,这个Actor可以是:服务端的PlayerController,或者已经被该PlayerController执行过Possess过的Character,或者执行过SetOwner且Owner为拥有服务端连接的Actor。

TestActor = GetWorld()->SpawnActor(ATestActor::StaticClass()); TestActor->SetReplicates(true); TestActor->SetAutonomousProxy(true); TestActor->SetOwner(NewPlayer);

两点注意事项: 1,上述三个步骤中,SetReplicates(true)必须最先执行,否则Actor在客户端的复本仍然是ROLE_SimulatedProxy; 2,SetAutonomousProxy(true);在构造函数中执行无效,必须在Spawn完成之后执行,否则Actor在客户端的复本仍然是ROLE_SimulatedProxy。

方式二:使用拥有服务端连接的PlayerController去Possess一个Character,Possess之前执行Actor->SetReplicates(true);。注意:ROLE_Authority类型的PlayerController才有权限执行Possess。服务端的PlayerController,其Role类型都为ROLE_Authority。 上文中的Possess堆栈,APawn::PossessedBy(AController* NewController)内部已经帮你执行了Actor->SetAutonomousProxy(true)和Actor->SetReplicates(true);。

想要了解UE4是如何判断对象是否拥有服务端连接的底层原理,可调试函数AActor::IsNetRelevantFor()

NetMode 和 Role 关系概述 当 NetMode 为NM_Standalone或者NM_DedicatedServer时,所有对象的Role都为ROLE_Authority。 当 NetMode 为 NM_Client 时,服务端 Spawn 出来的所有 Character 对象,在每个客户端的Role都为 ROLE_SimulatedProxy;PlayerController 比较特殊:当客户端连接服务器成功后,PlayerController 的 Role 自动变为 ROLE_AutonomousProxy ; Client function 没有在 Client 触发的问题

如果调用 Client function 的对象是在 Server (NM_DedicatedServer) 创建的,默认情况下,该对象上的 Client function 始终会在 Server 执行,且 Client (NM_Client) 不会触发。 如何让在Server上创建的对象的Client function只在Client (NM_Client) 执行?答案:SetOwner(),Owner为拥有服务端连接的对象,或者被有服务端连接的PlayerController执行过Possess的对象(如果对象是Pawn的话)。 如果是非 Server 创建的对象,比如:PlayerController ,那么其内部的 Client function 会在客户端执行。 如何在 Server 上获取这个 Client 的 PlayerController : 重写 AGameMode::InitNewPlayer() 或者 AGameMode::PostLogin() ,PlayerController 会作为参数传递进来,将这个 PlayerController 指针保存下来。

NetMulticast function 没有在 Client 触发的问题

并不是定义了 NetMulticast function ,就一定会在 Client 执行。 比如,在服务端生成了一个 Actor ,且在服务端执行该 Actor 上的 Multicast function ,默认情况下该 Multicast function 只会在 Server 执行。 如果要使该 Actor 的 Multicast function 在客户端也能够执行,分两种情况:

如果是手榴弹这种属于某个角色的Actor:

除了对该 Actor 执行 bReplicates = true; 外,还要执行 bNetUseOwnerRelevancy = true; 该 Actor Spawn 之后,需要执行 Actor->SetOwner(NewOwner);,这个 NewOwner 是一个 Replicated 对象,比如 Character 。NewOwner对象必须拥有服务端连接。对应的Get接口为:GetOwner()。

如果是类似场景机关这种对所有玩家共享的Actor,构造函数中执行:

bReplicates = true; bOnlyRelevantToOwner = false; 关键函数

1,客户端登陆

void UMyUserWidget::OnBtnLoginClick() { if (APlayerController* PC = GetWorld()->GetFirstPlayerController()) { FString URL = FString::Printf(TEXT("%s:%s?Alias=%s"), *(TxtServerIP->GetText().ToString()), *(TxtServerPort->GetText().ToString()), *(TxtUsername->GetText().ToString())); PC->ClientTravel(*URL, TRAVEL_Absolute); } }

拒绝非法登陆请求:

void AFPSGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) { Super::PreLogin(Options, Address, UniqueId, ErrorMessage); FString UserName = UGameplayStatics::ParseOption(Options, TEXT("UserName")).TrimStart().TrimEnd(); if (UserName.Len() == 0 || UserMap.Find(UserName)) { //deny client's login request. ErrorMessage = TEXT("User login repeatly!"); } }

将ErrorMessage设置为非空字符串,就表示拒绝客户端的链接。

2,GameMode::InitNewPlayer(),处理登陆请求时的自定义参数,比如账号名和密码。

FString AReplTestGameMode::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, const FString& Options, const FString& Portal) { FString Rs = Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal); if (AReplTestPlayerController* RTPC = Cast(NewPlayerController)) { FString Alias = UGameplayStatics::ParseOption(Options, TEXT("Alias")).Trim(); if (Alias.Len() == 0 || AliasMap.Find(Alias)) { return Rs; } RTPC->SetPlayerAlias(Alias); PlayerData Data; Data.Alias = Alias; AliasMap.Add(Alias, Data); } return Rs; }

3,GameMode::PostLogin(),登陆完成后的回调函数,创建角色可以放在这个函数中处理。

void AReplTestGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); if (GetNetMode() == NM_DedicatedServer) { PlayerCount++; if (CharClass) { if (AReplTestCharacter* Player = GetWorld()->SpawnActor(CharClass, SpawnLoc, SpawnRot)) { PlayerList.Add(Player); /*if (AMyAIController* AIC = GetWorld()->SpawnActor(SpawnLoc, SpawnRot)) { AIC->Possess(Player); }*/ Player->SpawnDefaultController(); //设置角色的显示名称 if (AReplTestPlayerController* RTPC = Cast(NewPlayer)) { if (PlayerData* Data = AliasMap.Find(RTPC->PlayerAlias())) { Player->SetAlias(*(Data->Alias)); Data->RTC = Player; if (AMyAIController* AIC = Cast(Player->GetController())) { AIC->SetPlayerAlias(Data->Alias); } } } } } } }

4,Character::BeginPlay(),当创建的Character进入场景时的回调函数,绑定摄像机的逻辑可以放在这个函数中

void AReplTestCharacter::BeginPlay() { Super::BeginPlay(); if (ROLE_SimulatedProxy == Role && NM_Client == GetNetMode()) { if (APlayerController* PC = GetWorld()->GetFirstPlayerController()) { if (AReplTestPlayerController* RTPC = Cast(PC)) { //因为一个客户端首次加载时会有多个玩家的角色进入场景,这里判断哪个角色才是当前客户端的 if (Alias_ == RTPC->PlayerAlias()) { PC->SetViewTarget(this); RTPC->SetSimulatedCharacter(this); } } } } }

5,客户端判断鼠标点击事件,这里加了一个保护,如果鼠标前后两次点击的坐标距离相差小于120,则不向服务端发送位移请求,防止频繁点击时发送消息太频繁。

void AReplTestPlayerController::MoveToMouseCursor() { // Trace to see what is under the mouse cursor FHitResult Hit; GetHitResultUnderCursor(ECC_Visibility, false, Hit); if (Hit.bBlockingHit) { if (FVector::Dist(LastDestLoc, Hit.ImpactPoint) > 120) { LastDestLoc = Hit.ImpactPoint; SetNewMoveDestination(Hit.ImpactPoint); } } }

6,服务端处理Move请求的函数,ServerMoveToDest_Validate判断请求的逻辑是否合法:Pawn是不是当前客户端操控的角色,防止操控其他玩家的角色。

bool AReplTestPlayerController::ServerMoveToDest_Validate(APawn* Pawn, const FVector DestLocation) { //判断请求是否非法,不允许当前客户端操控其他客户端的角色 bool Rs = false; if (AReplTestCharacter* RTC = Cast(Pawn)) { Rs = RTC->Alias() == PlayerAlias(); } return Rs; } void AReplTestPlayerController::ServerMoveToDest_Implementation(APawn* Pawn, const FVector DestLocation) { if (AMyAIController* AIC = Cast(Pawn->GetController())) { AIC->MoveToLocation(DestLocation); } } 注意事项:

1,Replicated属性只能在服务端修改 Replicated属性只允许服务端修改后通知客户端,而不允许客户端修改Replicated属性后通知到服务端。 参考:Only the server can replicate variables or multicast events to all clients https://answers.unrealengine.com/questions/459423/change-variable-in-client-want-server-to-see-it-bu.html

2,Server或者Client函数参数只能是指针或者引用,而不能是对象。 比如:假设参数是FString,那么必须是引用:const FString& Str,而不能是FString对象。

3,HUD的构造函数在服务端也会执行,但是DrawHUD()函数不会在服务端执行。 也就是说你要在HUD中判断当前程序是客户端还是服务端,可以不用考虑DrawHUD()函数。

凡是只需要客户端执行的逻辑,比如创建材质、修改颜色、加载贴图等等,一定要将这些逻辑单独封装成函数、且不要和数据更新的逻辑混在一起、且要确保这些函数只在客户端执行。 如何确保某个函数只在客户端执行:最直接最安全的方式是直接判断if(ENetMode::NM_Client == GetNetMode())。优雅一点的方式是通过Client function修饰这些逻辑函数,前提是你能确保这些函数所在的对象一定拥有了服务端连接。

4,当客户端登陆成功后,客户端所有的对象都会被重置,登陆前设置的对数属性值将变为默认值。 比如在启动游戏后登陆之前,你给PlayerController上的string属性设置为"abc",那么登陆成功后,这个属性值就变成了空字符串。

5,GameMode、AIController是为服务端设计的,PlayerController是为客户端设计的。两者虽然在客户端和服务端都可访问,但是前者是在服务端创建的,后者是在客户端创建的,比如PlayerController::GetHitResultAtScreenPosition只在客户端生效,而GameMode::PostLogin()只在服务端生效。

6,服务端的AIController::MoveToLocation()的效果相当于客户端UNavigationSystem::SimpleMoveToLocation()。AIController::MoveToLocation()内部有去调用NavigationSystem相关的接口。

7,客户端的PlayerController可以不用Possess玩家角色,因为客户端相关数据都是以服务端为准,操作角色也是在服务端完成,一般只需要对该角色绑定摄像机即可。Possess的意义之一是为了给 Pawn 赋予访问服务端函数的权限。

8,如果_Validate()函数返回false,则服务器会会认为客户端非法,并主动断开该客户端。断开客户端时服务端会打印:

LogRep: Error: ReceivedRPC: RPC_GetLastFailedReason: XXXX_Validate LogNet: Error: UActorChannel::ProcessBunch: Replicator.ReceivedBunch failed. Closing connection. RepObj: ReplTestPlayerController /Game/TopDownCPP/Maps/TopDownExampleMap.TopDownExampleMap:PersistentLevel.ReplTestPlayerController_1, Channel: 2

9,当前客户端只能获取当前控制角色的Controller,无法获取其他客户端的Controller,比如玩家A在玩家B的角色为C1,那么调用C1->GetController()时返回NULL。

10,动画同步 角色X在客户端A播放一个攻击动作,并且角色X在客户端B的视野内,此时需要客户端B也能同步看到角色X的动画,流程如下: 客户端发送请求 -》 服务端执行函数(假设叫ServerPlayAnim()) -》 ServerPlayAnim函数内调用NetMulticast函数 -》 NetMulticast函数内执行播放动画的逻辑。

11,编辑器模式下,即使勾选了 Run Dedicated Server,默认是不会自动连接专用服务器的,如果希望游戏启动时就自动连接专用服务器,需要勾选:Auto Connect to Server (Project Settings -> Level Editor -> Play -> Multiplayer Options),如果你自己实现了登陆逻辑,那么就不要勾选这个。如果是为了方便测试跳过登陆,可以勾选这个选项,并实现对应逻辑。

12,当客户端向服务端传递Rotator时,如果Rotator的值范围为:-180到180,那么传递到服务端时会被自动修改为:0到360。(v4.22)

13,此条内容可以无视,只是个人的备忘录。因为个人还在研究中,未找到正确方案 在服务端创建的Actor,如何让其复制到客户端、或者禁止复制到客户端? 3种情况:

只存在于服务端,不复制到客户端

Actor->SetReplicates(false); Actor->SetAutonomousProxy(false); Actor->bNetUseOwnerRelevancy = false; Actor->SetOwner(nullptr);

存在于服务端,并复制到客户端,但客户端服务端均无权限调用对方的远程函数

Actor->SetReplicates(true); Actor->SetAutonomousProxy(false); Actor->bNetUseOwnerRelevancy = false; Actor->SetOwner(拥有服务端连接的对象);

存在于服务端,并复制到客户端,客户端服务端都拥有权限调用对方的远程函数

Actor->SetReplicates(true); Actor->SetAutonomousProxy(true); Actor->bNetUseOwnerRelevancy = true; Actor->SetOwner(拥有服务端连接的对象);

14,打包版本的相关回调函数触发先后顺序问题

打包版本和PIE模式的Dedicated Server存在很大差异,比如:Actor的BeginPlay函数触发先后顺序,在PIE模式下,服务端生成的Actor,对应的客户端Actor的回调函数执行时机是固定的;而打包版本则不同,服务端生成的Actor,对应的客户端Actor的回调函数执行时机是随机的。所以建议两条:

完成的功能尽早打包,并以打包版本为准,在PIE模式下测试没有问题,很可能在打包版本下是截然不同的效果。PIE模式下,多关注下控制台的log,看看相关警告,一些隐藏问题可能已经有log提示。 服务端客户端交互时,两者均不要依赖引擎的回调函数去通知上层逻辑,而是主动抛出通知。比如:客户端中,Actor A 初始化时需要Actor B的数据,那么不要在Actor A的BeginPlay函数去获取Actor B,也不要Actor B的BeginPlay函数中去通知Actor A;而是当Actor A初始化完毕后,主动向中心管理器(比如GameMode或者PlayerController等)发送通知,表明自己已经初始化完毕,当Actor B初始化完毕时亦如此,然后当中心管理器中接收到两者都已初始化完毕的通知后,再主动通知Actor A,告诉他,你现在可以获取Actor B了。 以PlayerController为例,假设在服务端的AGameModeBase::PostLogin(APlayerController* NewPlayer)函数创建了一个开启了同步的Actor A,在PIE模式下,客户端的PlayerController::BeginPlay的触发时刻,要先于Actor A的客户端BeginPlay执行,而在打包版本下执行顺序则相反。

15,服务端创建的对象,在客户端的BeginPlay函数触发时,GetOwner()可能为空

不要在客户端的BeginPlay中获取Owner,有时候为空,有时候却不为空:PIE模式下不为空,但是打包版本不确定。

Owner发生变化时,AActor::OnRep_Owner()会被触发。AActor::OnRep_Owner()只在客户端触发。

16,NM_Client模式下,UGameplayStatics::GetGameMode()返回为null,即使参数WorldContext有效;UGameplayStatics::GetGameInstance()表现正常,可以获取到有效的GameInstance对象。

常见问题:

如果出现以下错误,表示Reliable函数的参数名和引擎生成的代码有同名的情况,把参数名重新改一下即可。

error : Function parameter: ‘Pawn’ cannot be defined in ‘ServerMoveToDest’ as it is already defined in scope ‘Controller’ (shadowing is not allowed)

如果服务端SpawnActor时返回NULL,且参数传递都正确,可能是服务端上的对应Actor未清理,比如客户端崩掉了,导致了服务端的Actor未即时清理。

执行UNavigationSystem::SimpleMoveToLocation(MyCharacter()->GetController(), Location);时位移无效(假设已经生成了NavMesh)。原因是MyCharacter是在客户端生成的,客户端PlayerController传递给NavigationSystem()时执行无效,需要在服务端Spawn这个Character,然后再给其Spawn出一个AIController并Possess。官方模版项目,传递给NavigationSystem的是PlayerController且位移有效,是因为模版项目中设置的DefautlPawnClass,其实是服务端Spawn出来的Character,执行位置也是在服务端。

在打包版本下,客户端的玩家行走时,摄像机有抖动和卡顿(编辑器模式下正常),原因是没有勾选SpringArmComponent的Lag属性:Enable Camera Lag、Enable Camera Rotation Lag Dedicated Servers, Jitter, Matchmaking https://forums.unrealengine.com/development-discussion/c-gameplay-programming/96598-dedicated-servers-jitter-matchmaking

旋转视角时,NM_Standalone 模式下执行 APlayerController::AddYawInput() 或者 APawn::AddControllerYawInput() 没有问题,但是 NM_Client 模式下失效。 原因: 可能是Use Controller Rotation Yaw设置成了false。 解决办法: 有些情况下,我们不希望使用 Controller Rotation Yaw 作为角色的朝向,那么此时想旋转角色方向,使用如下方式:

void AMyPlayerController::Turn(float Rate) { // calculate delta for this frame from the rate information if (Rate != 0.f) { if (AActor* Target = GetViewTarget()) { Target->AddActorWorldRotation(FRotator(0.f, Rate * InputYawScale, 0.f)); } } }

Client连接Dedicated Server的情况下,Client执行APlayerController::AddYawInput()失效,是因为此时Client的PlayerController没有Possess对应的Character。如果在服务端使用PlayerController对Character执行Possess,那么就可以使用APlayerController::AddYawInput()控制摄像机转向,以及使用APawn::AddMovementInput()控制玩家移动。但是要注意,Possess之后,该Character就拥有了访问服务端函数的权限。

假设角色蓝图中有两个Component:A 和 B,A 设置为 A->SetOnlyOwnerSee(true),且 B 附加在 A 上: B->SetupAttachment(A),那么只能第一个登陆的角色正常,之后登陆的角色会消失不见。 解决办法: 这种情况下,A 不要 Attach B,如果要 Attach,B 设置为 SetOnlyOwnerSee(false)(默认为false);或者 A 也隐藏掉。

如果角色身上 Attach 了一个 BoxComponent 或者 SphereComponent,那么这个Component的 CollisionProfileName 不要设置为 Projectile(First Person Shooter模版项目中的自定义 Collision Channel),(能否设置为其他没试过,最好默认),如果设置成 Projectile,那么角色在移动时会不停抖动(Standalone是否也有这种问题没试过)。

DedicatedServer 模式下,SpawnActor生成出的 Actor,且这个Actor bReplicates 设置为true, 执行ConditionalBeginDestroy()时,一定时间后客户端会崩掉。 解决办法: SpawnActor生成出的 Actor,且改 Actor 同步到远程机器,销毁时,不要用ConditionalBeginDestroy(),而要用Destroy()。Standalone 模式貌似没这种限制。 崩溃日志:

Assertion failed: Index >= 0 [File:D:\Build\++UE4+Release-4.16+Compile\Sync\Engine\Source\Runtime\CoreUObject\Public\UObject/UObjectArray.h] [Line: 455] UE4Editor_Core!FDebug::AssertFailed() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\private\misc\assertionmacros.cpp:349] UE4Editor_Engine!UNetDriver::ServerReplicateActors_BuildConsiderList() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:2600] UE4Editor_Engine!UNetDriver::ServerReplicateActors() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:3159] UE4Editor_Engine!UNetDriver::TickFlush() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\networkdriver.cpp:355] UE4Editor_Engine!TBaseUObjectMethodDelegateInstance::ExecuteIfSafe() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\public\delegates\delegateinstancesimpl.h:858] UE4Editor_Engine!TBaseMulticastDelegate::Broadcast() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\core\public\delegates\delegatesignatureimpl.inl:937] UE4Editor_Engine!UWorld::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\engine\private\leveltick.cpp:1506] UE4Editor_UnrealEd!UEditorEngine::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\editor\unrealed\private\editorengine.cpp:1633] UE4Editor_UnrealEd!UUnrealEdEngine::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\editor\unrealed\private\unrealedengine.cpp:386] UE4Editor!FEngineLoop::Tick() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\launchengineloop.cpp:3119] UE4Editor!GuardedMain() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\launch.cpp:166] UE4Editor!GuardedMainWrapper() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:134] UE4Editor!WinMain() [d:\build\++ue4+release-4.16+compile\sync\engine\source\runtime\launch\private\windows\launchwindows.cpp:210] UE4Editor!__scrt_common_main_seh() [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:253] kernel32 ntdll

USkeletalMeshComponent::SetSkeletalMesh()和UStaticMeshComponent::SetStaticMesh()不会Replicate。 在服务端执行这两个函数并不会将修改同步到客户端,官方推荐方式是:修改Mesh的时候,向客户端发送消息,在客户端修改Mesh。这个bug从4.7版本就出现了,且官方也不准备修复,因为不建议在服务端修改Mesh,毕竟只是显示相关的功能。 跟显示相关的逻辑尽量全部放在客户端,包括创建SkeletalMeshComponent的逻辑。 参考:change skeletal mesh, blueprint, multiplayer https://answers.unrealengine.com/questions/328659/change-skeletal-mesh-blueprint-multiplayer.html?sort=oldest

Client function 和 Multicast function的可执行时机 同一帧内:在服务端创建的Actor,SpawnActor之后,接着执行SetOwner,然后调用Client function,客户端函数就可以触发。 但是如果调用 Multicast function,只会在服务端触发,客户端不会触发。什么时候调用 Multicast function才会在客户端触发? 答案:该Actor在客户端的BeginPlay()函数触发的时候,也就是要等这个服务端创建的Actor在客户端Replicate完成之后。

使用ClientTravel连接远程服务器,连接成功后,如果客户端没有被服务器通知切换场景,那么当前客户端的GameInstance,GameMode不会被重置,还是游戏启动时的对象,但PlayerController会被重置(触发BeginPlay函数)。 若服务端通知客户端切换场景,除了GameInstance之外,其他对象都会被重置,所以如果有些数据需要所有场景都公用的,可以放在GameInstance,否则就必须在各个游戏场景中重新设置这些数据。

如何让Replicated属性同步到所有客户端? 默认情况下,UPROPERTY(Replicated)标识的属性只会同步给自己的Owner,可以理解为:只同步给Actor所属的客户端,其他客户端连上来,获取不到这个属性。 如何让Replicated属性同步给所有客户端?执行:bOnlyRelevantToOwner = false;。默认为true,表示只同步给自己的Owner。

官方文档说在运行时期间修改bOnlyRelevantToOwner对其他客户端无效,需要其他客户端重连才会生效。官方文档不一定准确,待验证。。

bOnlyRelevantToOwner的另一个用途:服务端启动时(比如:GameMode的BeginPlay函数中)创建的对象,即使bReplicates = true;,客户端连上来之后,这些对象不会同步到客户端,如何让客户端登陆成功后自动获取到服务端中已经存在的对象?答案:将这些服务端对象的bOnlyRelevantToOwner设置为false。

bAlwaysRelevant与bOnlyRelevantToOwner关系: 当bOnlyRelevantToOwner = false;时,Replication的距离裁剪仍然会执行,但bAlwaysRelevant = true;时,则Actor将全场景同步,所以需要严格限制bAlwaysRelevant = true;的Actor数量。

Replicated属性如何选择性的同步给指定客户端? 使用DOREPLIFETIME_CONDITION修饰同步属性,比如COND_SkipOwner就表示同步给除Owner以外的所有客户端。

DOREPLIFETIME_CONDITION(AWeaponModule, CurrWeapon, COND_SkipOwner);

如果一个Actor没有SetRootComponent,且没有继承蓝图(蓝图会默认创建一个DefaultComponent作为RootComponent),即使开启同步,也不会同步到客户端。 如果Actor不需要任何Component,建议在给其创建一个空白Component作为RootComponent。

AMyActor::AMyActor() { bReplicates = true; EmptyRootComp = CreateDefaultSubobject(TEXT("EmptyRootComp")); if (EmptyRootComp) { SetRootComponent(EmptyRootComp); } }

建议为没有Component的纯C++ Actor创建一个RootComponent,否则AActor::IsNetRelevantFor()始终返回false,会跳过Replication的距离裁剪,增加同步开销。

如果一个Actor开启了同步,但又期望Actor的Component只在DedicatedServer中创建,这种需求无法实现,因为Actor开启同步后,如果只在Server端创建Component,此时Component会被自动置空。

服务端的SkeletalMesh上Socket坐标和客户端不同步的问题。 问题现象: 客户端服务端分别获取的GetSocketTransform()值不一致,有很大的误差。 可能原因: 1,服务端的Bone Fresh关闭了。 解决办法:

Mesh->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones;

2,动画状态不同步,比如:客户端的当前动画是持手枪,但是服务端的当前动画是持狙击。 解决办法: 切换动画时确保客户端服务端同时执行。

射击游戏中,如果子弹是服务端生成,并同步到客户端,并且子弹是真实弹道(不是射线检测),并且在子弹的Hit事件中Destroy自己,那么当子弹速度过快时,且发射口离障碍物很近时,子弹还没来得及同步到客户端,子弹在服务端就已经消掉掉了,从而导致Hit事件只会在服务端触发,不会在客户端触发。而当发射口离障碍物有一定距离时(距离取决与子弹速度),客户端和服务端的Hit会同时触发。

销毁Actor时(接口:bool AActor::Destroy(bool bNetForce = false, bool bShouldModifyLevel = true)),如果需要将bNetForce设置为true才能销毁(这种情形用在强制销毁客户端的actor),则说明DS有内存泄漏问题。因为bNetForce只是将客户端的actor销毁,服务端的actor内存还没释放。为了客户端表现,可以提前Destroy(true);销毁客户端actor,但对应的服务端actor一定也要有Destroy();。

示例工程下载地址

这个工程涉及内容少一些,适合UE4初学者研读。(里面有些API可能已经过时,以文章内容为准) http://pan.baidu.com/s/1o7MzmRo

如果对UE4有一定了解,可以研读下面工程,涵盖了UE4 Dedicated Server的关键知识点。(这个工程是我在2017年参加阿姆斯特丹某游戏工作室的笔试后整理的笔记) https://github.com/dawnarc/ue4_fps_game

参考文章

属性同步: http://blog.csdn.net/yangxuan0261/article/details/54766955

RPC: http://blog.csdn.net/yangxuan0261/article/details/54766951

How To Test Dedicated Server Games Via Commandline https://wiki.unrealengine.com/How_To_Test_Dedicated_Server_Games_Via_Commandline

Networking and Multiplayer https://docs.unrealengine.com/en-us/Gameplay/Networking/Replication

UE4基础:客户端服务器连接流程 http://wangjie.rocks/2019/03/29/ue4-client-ds-connect/

Unreal Networking Guide Created by Zach Metcalf http://www.zachmetcalfgames.com/wp-content/uploads/2014/12/zmg_Unreal_Networking_Guide.pdf

深入浅出UE4网络 https://www.cnblogs.com/leonhard-/p/6511821.html

Multiplayer in Unreal Engine: How to Understand Network Replication https://www.youtube.com/watch?v=JOJP0CvpB8w

天下事,成于精而败于众,立于诚而倾于奢。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有